import 'dart:async';
import 'dart:io' as io;
import 'dart:io' show exit;

import 'package:dispose_scope/dispose_scope.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' show basename;
import 'package:patrol_cli/src/base/logger.dart';
import 'package:patrol_cli/src/base/process.dart';
import 'package:patrol_cli/src/runner/flutter_command.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

class FlutterTool {
  FlutterTool({
    required Stream<List<int>> stdin,
    required ProcessManager processManager,
    required Platform platform,
    required DisposeScope parentDisposeScope,
    required Logger logger,
    this.onQuit,
  }) : _stdin = stdin,
       _processManager = processManager,
       _platform = platform,
       _disposeScope = DisposeScope(),
       _logger = logger {
    _disposeScope.disposedBy(parentDisposeScope);
  }

  final Stream<List<int>> _stdin;
  final ProcessManager _processManager;
  final Platform _platform;
  final DisposeScope _disposeScope;
  final Logger _logger;
  final Future<void> Function()? onQuit;

  var _hotRestartActive = false;
  var _logsActive = false;
  var _devtoolsUrl = '';

  /// Forwards logs and hot restarts the app when "r" is pressed.
  Future<void> attachForHotRestart({
    required FlutterCommand flutterCommand,
    required String deviceId,
    required String target,
    required String? appId,
    required Map<String, String> dartDefines,
    required bool openDevtools,
    bool attachUsingUrl = false,
    Future<void> Function()? onQuit,
  }) async {
    StdinModes? previousStdinModes;
    if (io.stdin.hasTerminal) {
      previousStdinModes = enableInteractiveMode();
    }

    Future<void> onQuitWithRevertInteractiveMode() async {
      if (previousStdinModes != null) {
        revertInteractiveMode(previousStdinModes);
      }
      if (onQuit != null) {
        await onQuit();
      }
    }

    if (attachUsingUrl) {
      final urlCompleter = Completer<String>();
      await logs(
        deviceId,
        flutterCommand: flutterCommand,
        observationUrlCompleter: urlCompleter,
      );
      final url = await urlCompleter.future;
      await attach(
        flutterCommand: flutterCommand,
        target: target,
        deviceId: deviceId,
        appId: appId,
        debugUrl: url,
        dartDefines: dartDefines,
        openBrowser: openDevtools,
        onQuit: onQuitWithRevertInteractiveMode,
      );
    } else {
      await Future.wait<void>([
        logs(deviceId, flutterCommand: flutterCommand),
        attach(
          flutterCommand: flutterCommand,
          target: target,
          deviceId: deviceId,
          appId: appId,
          dartDefines: dartDefines,
          openBrowser: openDevtools,
          onQuit: onQuitWithRevertInteractiveMode,
        ),
      ]);
    }
  }

  /// Attaches to the running app. Returns a [Future] that completes when the
  /// connection is ready.
  ///
  /// If [openBrowser] is true, Dart DevTools (with Patrol extension page
  /// selected) will be automatically opened in the browser once DevTools URL is
  /// printed by Flutter CLI.
  @visibleForTesting
  Future<void> attach({
    required FlutterCommand flutterCommand,
    required String deviceId,
    required String target,
    String? debugUrl,
    required String? appId,
    required Map<String, String> dartDefines,
    required bool openBrowser,
    Future<void> Function()? onQuit,
  }) async {
    await _disposeScope.run((scope) async {
      final process =
          await _processManager.start([
              ...[flutterCommand.executable, ...flutterCommand.arguments],
              'attach',
              '--no-version-check',
              '--suppress-analytics',
              '--debug',
              ...['--device-id', deviceId],
              if (debugUrl != null) ...['--debug-url', debugUrl],
              if (appId != null) ...['--app-id', appId],
              ...['--target', target],
              for (final dartDefine in dartDefines.entries) ...[
                '--dart-define',
                '${dartDefine.key}=${dartDefine.value}',
              ],
            ])
            ..disposedBy(scope);

      final completer = Completer<void>();
      scope.addDispose(() {
        if (!completer.isCompleted) {
          _logger.detail('Killed before attached to the app');
          completer.complete();
        }
      });

      _stdin
          .listen((event) async {
            final char = String.fromCharCode(event.first);
            if (char == 'r' || char == 'R') {
              if (!_hotRestartActive) {
                _logger.warn('Hot Restart: not attached to the app yet!');
                return;
              }

              _logger.success(
                'Hot Restart for entrypoint ${basename(target)}...',
              );
              process.stdin.add('R'.codeUnits);
            } else if (char == 'h' || char == 'H') {
              final helpText = StringBuffer(
                'Patrol develop key commands:\n'
                'r Hot restart\n'
                'h Print this help message\n'
                'q Quit (terminate the process and application on the device)',
              );

              if (_devtoolsUrl.isNotEmpty) {
                helpText.writeln('\nDevTools: $_devtoolsUrl');
              } else {
                helpText.writeln('\nDevTools: not available yet');
              }

              _logger.success(helpText.toString());
            } else if (char == 'q' || char == 'Q') {
              _logger.success('Quitting process...');
              process.kill();
              if (!completer.isCompleted) {
                completer.complete();
              }

              // Call the uninstall function if provided
              if (onQuit != null) {
                try {
                  await onQuit();
                } catch (err) {
                  _logger.err('Failed to clean up app: $err');
                }
              }

              exit(0);
            }
          })
          .disposedBy(scope);

      _logger.detail('Hot Restart: waiting for attach to the app...');
      process
          .listenStdOut((line) {
            if (line == 'Flutter run key commands.' && !completer.isCompleted) {
              _logger.success(
                'Hot Restart: attached to the app\n'
                'Patrol develop key commands:\n'
                'r Hot restart\n'
                'h Print this help message\n'
                'q Quit (terminate the process and application on the device)',
              );
              _hotRestartActive = true;

              if (!_logsActive) {
                _logger.warn('Hot Restart: logs are not connected yet');
              }
              completer.complete();
            }

            if (line.startsWith('The Flutter DevTools debugger and profiler')) {
              _devtoolsUrl = _getDevtoolsUrl(line);
              _logger.success(
                'Patrol DevTools extension is available at $_devtoolsUrl',
              );

              if (openBrowser) {
                unawaited(_openDevtoolsPage(_devtoolsUrl));
              }
            }

            _logger.detail('\t: $line');
          })
          .disposedBy(scope);

      process
          .listenStdErr((line) {
            if (line.startsWith('Waiting for another flutter command')) {
              // This is a warning that we can ignore
              return;
            }
            _logger.err('\t$line');
          })
          .disposedBy(scope);

      await completer.future;
    });
  }

  /// Connects to app's logs. Returns a [Future] that completes when the logs
  /// are connected.
  @visibleForTesting
  Future<void> logs(
    String deviceId, {
    required FlutterCommand flutterCommand,
    Completer<String>? observationUrlCompleter,
  }) async {
    await _disposeScope.run((scope) async {
      _logger.detail('Logs: waiting for them...');
      final process =
          await _processManager.start([
              ...[flutterCommand.executable, ...flutterCommand.arguments],
              '--no-version-check',
              '--suppress-analytics',
              'logs',
              '--device-id',
              deviceId,
            ], runInShell: true)
            ..disposedBy(scope);

      final completer = Completer<void>();
      scope.addDispose(() {
        if (!completer.isCompleted) {
          completer.complete();
        }
      });

      process
          .listenStdOut((line) {
            if (line.contains('Dart VM service')) {
              final url = _getObservationUrl(line);
              observationUrlCompleter?.complete(url);
            }
            if (line.startsWith('Showing ') && line.endsWith('logs:')) {
              _logger.success('Hot Restart: logs connected');
              _logsActive = true;

              if (!_hotRestartActive) {
                _logger.warn('Hot Restart: not attached to the app yet');
              }
              completer.complete();
            }

            // Skip the log line that contains "PATROL_LOG" prefix
            const patrolLogPrefix = 'PATROL_LOG';
            if (line.contains(patrolLogPrefix)) {
              return;
            }

            // On iOS, "flutter" is not prefixed
            final flutterPrefix = RegExp('flutter: ');

            // On Android, "flutter" is prefixed with "I\"
            final flutterWithPortPrefix = RegExp(r'I\/flutter \(\s*[0-9]+\): ');
            if (line.startsWith(flutterWithPortPrefix)) {
              _logger.info('\t${line.replaceFirst(flutterWithPortPrefix, '')}');
            } else if (line.startsWith(flutterPrefix)) {
              _logger.info('\t${line.replaceFirst(flutterPrefix, '')}');
            } else {
              _logger.detail('\t$line');
            }
          })
          .disposedBy(scope);

      process.listenStdErr((l) => _logger.err('\t$l')).disposedBy(scope);

      await completer.future;
    });
  }

  /// Enables interactive mode. Returns the previous stdin modes.
  StdinModes enableInteractiveMode() {
    final stdinModes = StdinModes(
      echoMode: io.stdin.echoMode,
      lineMode: io.stdin.lineMode,
    );

    // Prevents keystrokes from being printed automatically. Needs to be
    // disabled for lineMode to be disabled too.
    io.stdin.echoMode = false;

    // Causes the stdin stream to provide the input as soon as it arrives (one
    // key press at a time).
    io.stdin.lineMode = false;

    _logger.detail('Interactive shell mode enabled.');

    return stdinModes;
  }

  void revertInteractiveMode(StdinModes stdinModes) {
    io.stdin.echoMode = stdinModes.echoMode;
    io.stdin.lineMode = stdinModes.lineMode;

    _logger.detail('Interactive shell mode disabled.');
  }

  Future<void> _openDevtoolsPage(String url) async {
    io.Process? process;
    switch (_platform.operatingSystem) {
      case Platform.macOS:
        process = await _processManager.start(['open', url]);
      case Platform.windows:
        process = await _processManager.start(['start', url], runInShell: true);
      case Platform.linux:
        process = await _processManager.start(['xdg-open', url]);
    }

    await process?.exitCode;
  }

  String _getDevtoolsUrl(String line) {
    final url = _getObservationUrl(line);
    return url.replaceAllMapped('?uri=', (_) => '/patrol_ext?uri=');
  }

  String _getObservationUrl(String line) {
    final startIndex = line.indexOf('http');
    return line.substring(startIndex);
  }
}

class StdinModes {
  StdinModes({required this.echoMode, required this.lineMode});

  final bool echoMode;
  final bool lineMode;
}
